package pl.com.qapps.unity;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;

import pl.com.qapps.unity.UnityPb.Request;
import pl.com.qapps.unity.UnityPb.Response;
import pl.com.qapps.unity.UnityPb.Response.Type;
import pl.com.qapps.util.concurrent.FakeFuture;

import com.google.protobuf.ExtensionRegistry;
import com.google.protobuf.GeneratedMessage.GeneratedExtension;

/**
 * @author Jakub Dykowski
 * 
 */
public final class Unity {

	private static final Logger log = Logger.getLogger(Unity.class.getName());
	private static final Properties settings = new Properties();

	private Unity() {
	}

	static {
		try {
			log.setLevel(Level.OFF);
		} catch (SecurityException e) {
			e.printStackTrace(System.err);
		}
		try {
			// load properties
			settings.load(Unity.class.getResourceAsStream("unity.properties"));
			// extract scripts if not present
			Path dir = Paths.get(settings.getProperty("script.dir"));
			Files.createDirectories(dir);
			for (String resource : settings.getProperty("script.resources")
					.split(";")) {
				Path script = dir.resolve(resource);
				if (Files.notExists(script, LinkOption.NOFOLLOW_LINKS)) {
					log.fine("extracting script: " + resource + " to "
							+ dir.toString());
					Files.copy(Unity.class.getResourceAsStream(resource),
							script, StandardCopyOption.REPLACE_EXISTING);
				}
			}
		} catch (IOException e) {
			log.severe("cannot initialize libunity:" + e.getStackTrace());
			throw new UnityException("cannot initialize libunity", e);
		}
	}

	static String getProperty(String name) {
		return settings.getProperty(name);
	}

	private static final ExtensionRegistry extensionRegistry = ExtensionRegistry
			.newInstance();

	private static final Collection<InitHandler> initHandlers = new ArrayList<>();

	private static final Collection<SignalHandler> signalHandlers = new ArrayList<>();

	private static final EventDispatcher eventDispatcher = new EventDispatcher();

	private static InternalBus bus = new SmartBus();

	/**
	 * Once called, allocated necessary memory and made connection to Unity API.
	 * All following calls return rather the same instance, except when previous
	 * connection were broken then new one is made. The connection is shared by
	 * all services.
	 * 
	 * @return the connection bus to Unity API
	 * @throws IOException
	 *             if new connection failed to establish
	 */
	static synchronized Bus connect() throws IOException, IllegalStateException {
		return bus;
	}

	private static boolean supported = false;

	private static synchronized InternalBus newConnection() throws IOException {
		if (!supported && !isSupported()) {
			// TODO do better than a fake
			return new InternalBus() {

				@Override
				public Response send(Request message)
						throws IllegalStateException, IOException {
					throw new UnityException(
							"Unity is not supported by this operating system.");
				}

				@Override
				public void close() throws IOException {
				}

				@Override
				public boolean isClosed() {
					return false;
				}

			};
		} else {
			supported = true;
			UnityConnection bus = new UnityConnection();
			synchronized (initHandlers) {
				for (InitHandler init : initHandlers) {
					init.onInit(bus);
				}
			}
			return bus;
		}
	}

	/**
	 * Wraps actual bus. Retries after crash, also manages creating new
	 * connections itself. Makes any crash go almost transparently so no one
	 * will even notice it.
	 * 
	 * <p>
	 * ThreadSafe.
	 * </p>
	 * 
	 * @author Jakub Dykowski
	 * 
	 */
	private static class SmartBus implements InternalBus {

		static final int MAX_ATTEMPT_NUMBER = 3;
		InternalBus bus;
		int attempt;
		final Lock lock = new ReentrantLock(true);

		@Override
		public Response send(Request message) throws IllegalStateException,
				IOException {
			lock.lock();
			try {
				// TODO initialization is prone to crash, fix it
				if (bus == null || bus.isClosed()) {
					if (bus != null)
						bus.close();
					bus = newConnection();
					// TODO ensure that is correct
					attempt = 1;
				}
				Response res;
				try {
					res = bus.send(message);
				} catch (IOException e) {
					if (attempt > MAX_ATTEMPT_NUMBER) {
						attempt = 1;
						throw e;
					}
					log.log(Level.WARNING,
							"suppressing error and making another attempt("
									+ attempt + ")", e);
					++attempt;
					return send(message);
				}
				return res;
			} finally {
				lock.unlock();
			}
		}

		public boolean isClosed() {
			return bus == null ? true : bus.isClosed();
		}

		@Override
		public void close() throws IOException {
			lock.lock();
			try {
				if (bus != null)
					bus.close();
			} finally {
				lock.unlock();
			}
		}
	}

	/**
	 * The connection to Unity API, one for all features.<br>
	 * <br>
	 * NotThreadSafe.
	 * 
	 * @author Jakub Dykowski
	 * 
	 */
	private static class UnityConnection implements InternalBus {

		private Thread receiver;
		private final Pipe pipe;
		private final Process process;
		private boolean closed = false;
		// TODO consider size of the queue or changing entirely the approach
		private BlockingQueue<Response> responses = new LinkedBlockingQueue<>();

		private UnityConnection() throws IOException {
			List<String> args = new LinkedList<>();
			args.add(settings.getProperty("script.command"));
			Collections.addAll(args, settings.getProperty("script.options")
					.split(";"));
			args.add(settings.getProperty("script.dir") + "/"
					+ settings.getProperty("script.name"));
			log.info("running unity service script: " + args);
			process = new ProcessBuilder(args).start();

			InputStream in = process.getInputStream();
			int b = in.read();
			OutputStream out = process.getOutputStream();
			out.write(b);
			out.flush();
			pipe = new Pipe(out, in, extensionRegistry);

			receiver = new Thread(new Receiver());
			Runtime.getRuntime().addShutdownHook(new Thread() {

				@Override
				public void run() {
					try {
						close();
					} catch (IOException e) {
					}
				}
			});
			receiver.setDaemon(true);
			receiver.start();
		}

		@Override
		public Response send(Request message) throws IllegalStateException,
				IOException, UnityException {
			if (closed)
				throw new IllegalStateException("connection is closed");

			Response res;
			try {
				pipe.send(message);
				res = responses.take();
				if (res.hasException())
					throw new UnityException(res.getException());
				return res;
			} catch (InterruptedException e) {
				// TODO better handle the interruption
				throw new RuntimeException(
						"interrupted while waiting for response", e);
			}
		}

		// TODO consider the necessity of synchronization here
		public void close() throws IOException {
			if (!isClosed()) {
				this.closed = true;
				receiver.interrupt();
				pipe.close();
				process.destroy();
			}
		}

		public boolean isClosed() {
			return closed;
		}

		private class Receiver implements Runnable {

			@Override
			public void run() {
				// TODO make better exception handling
				try {
					Response res;
					while (!closed) {
						res = pipe.receive(Response.newBuilder());
						if (res.getType() == Type.SIGNAL)
							eventDispatcher.dispatch(res);
						else
							responses.offer(res);
					}
				} catch (IOException e) {
					log.log(Level.WARNING, "pipe to sub process broken", e);
				}
			}
		}
	}

	/**
	 * Checks whether operating system supports Unity, in other words if
	 * application is running on Ubuntu and libraries necessary to access to
	 * Unity are present.
	 * 
	 * @return <code>true</code> if supported, <code>false</code> otherwise
	 */
	public static boolean isSupported() {
		if ("linux".equalsIgnoreCase(System.getProperty("os.name"))
				&& "gnome".equalsIgnoreCase(System.getProperty("sun.desktop"))) {
			try {
				Process p = new ProcessBuilder().command(
						settings.getProperty("support.script.command"),
						settings.getProperty("script.dir") + "/"
								+ settings.getProperty("support.script.name"))
						.start();
				int value = p.waitFor();
				return value != 47;
			} catch (IOException | InterruptedException e) {
				throw new UnityException(e);
			}
		} else
			return false;
		// TODO check somehow if Unity is running on the OS which is of course
		// Ubuntu --->> use python Unity.Inspector
	}

	/**
	 * Registers the handler which is called each time new connection to Unity
	 * by the current VM is made (e.g. due to error) and before it is actually
	 * registered.
	 * 
	 * @param handler
	 * @throws UnityException
	 */
	static void register(InitHandler handler) throws UnityException {
		Objects.requireNonNull(handler, "handler cannot be null");
		try {
			handler.onInit(connect());
			synchronized (initHandlers) {
				initHandlers.add(handler);
			}
		} catch (IllegalStateException | IOException e) {
			throw new UnityException(
					"failed to apply initialization handler before it is registered",
					e);
		}

	}

	static void unregister(InitHandler handler) {
		synchronized (initHandlers) {
			initHandlers.remove(handler);
		}
	}

	/**
	 * Registers the handler for the lifetime of current VM.
	 * 
	 * @param handler
	 */
	static void register(SignalHandler handler) {
		if (handler != null)
			synchronized (signalHandlers) {
				signalHandlers.add(handler);
			}
	}

	/**
	 * Extensions that are to be returned from unity bus should be earlier
	 * registered, otherwise they are not going to be returned.
	 * 
	 * @param ext
	 */
	static void register(GeneratedExtension<?, ?> ext) {
		extensionRegistry.add(ext);
	}

	static Future<?> dispatch(Runnable evt) {
		return eventDispatcher.dispatch(evt);
	}

	/**
	 * Dispatches the event on special thread and awaits for termination,
	 * throwing UnityException with the exception as its cause. If current
	 * thread is the special thread then the event is simply executed within
	 * this thread.
	 * 
	 * @param evt
	 * @throws UnityException
	 */
	static void dispatchSynchronously(Runnable evt) throws UnityException {
		try {
			eventDispatcher.dispatch(evt).get();
		} catch (InterruptedException e) {
			throw new UnityException(e);
		} catch (ExecutionException e) {
			throw new UnityException(e.getCause());
		}
	}

	interface InitHandler {
		void onInit(Bus conn);
	}

	interface SignalHandler {
		void onSignal(Response res);
	}

	/**
	 * ThreadSafe.
	 * 
	 * @author Jakub Dykowski
	 * 
	 */
	interface Bus {
		Response send(Request message) throws IllegalStateException,
				IOException;
	}

	private interface InternalBus extends Bus, Closeable {
		boolean isClosed();
	}

	private static class EventDispatcher {

		private static final ThreadLocal<Boolean> IS_DISPATCH_THREAD = new ThreadLocal<Boolean>();
		private final ExecutorService executor = Executors
				.newSingleThreadExecutor(new ThreadFactory() {

					@Override
					public Thread newThread(final Runnable r) {
						Thread t = new Thread(new Runnable() {

							@Override
							public void run() {
								IS_DISPATCH_THREAD.set(Boolean.TRUE);
								r.run();
							}

						});
						t.setDaemon(true);
						return t;
					}

				});

		private EventDispatcher() {
		}

		// TODO move it out from this class, it specific use, and this class is
		// a utility
		/**
		 * Dispatches incoming signal in the event thread.
		 * 
		 * @param signal
		 * @return
		 */
		private Future<?> dispatch(final Response signal) {
			Runnable evt = new Runnable() {

				@Override
				public void run() {
					synchronized (signalHandlers) {
						for (SignalHandler handler : signalHandlers)
							try {
								handler.onSignal(signal);
							} catch (Throwable suppressed) {
								log.severe("failed handling signal: "
										+ suppressed.getLocalizedMessage());
							}
					}
				}

			};
			return executor.submit(evt);
		}

		/**
		 * Used to perform operations safely in dispatch thread. If callers
		 * thread is the dispatch thread action is performed synchronously.
		 * 
		 * Remember not to abuse this thread cause it will freeze user interface
		 * responsiveness.
		 * 
		 * @param evt
		 *            action to perform
		 * @return
		 */
		private Future<?> dispatch(Runnable evt) {
			if (isDispatchThread()) {
				evt.run();
				return new FakeFuture<Void>(null);
			} else
				return executor.submit(evt);
		}

		private static boolean isDispatchThread() {
			return IS_DISPATCH_THREAD.get() == null ? false
					: IS_DISPATCH_THREAD.get();
		}
	}
}
